/**
 *  Copyright (c) Filipe Manana <fdmanana@gmail.com>
 *  All rights reserved.
 * 
 *  This file is part of jsdet.
 *
 *  jsdet is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published
 *  by the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  jsdet is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with jsdet.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

package jsdet.parsing;


import java.util.*;

import jsdet.parsing.enums.ClassMemberType;


/**
 * Accumulates parsing statistics of multiple units (files) of JavaScript code.
 *
 * @author Filipe Manana
 *
 */
public class GlobalParsingStats {

    private static class ClassNameComparator implements Comparator<String> {
        public int compare(String s1, String s2) {
            int dotsNumber1 = getDotsNumber(s1);
            int dotsNumber2 = getDotsNumber(s2);

            // namespaces aware
            // classes with shorter namespaces are "smaller"
            if ( dotsNumber1 < dotsNumber2 ) {
                return -1;
            }
            else if ( dotsNumber1 > dotsNumber2 ) {
                return 1;
            }

            return s1.compareToIgnoreCase(s2);
        }
        private int getDotsNumber(String str) {
            int n = 0;
            Character dotChar = new Character('.');

            for ( int i = 0; i < str.length(); i++ ) {
                Character c = new Character(str.charAt(i));

                if ( c.equals(dotChar) ) {
                    n += 1;
                }
            }

            return n;
        }
    }

    private Set<String> files = new LinkedHashSet<String>();

    private Map<String, String> functionsFiles = new LinkedHashMap<String, String>();
    private Map<String, Set<String>> filesFunctions = new LinkedHashMap<String, Set<String>>();

    private Map<String, String> closureObjectFiles = new LinkedHashMap<String, String>();
    private Map<String, Set<String>> filesClosureObjects = new LinkedHashMap<String, Set<String>>();

    private Map<String, Set<String>> classesFiles = new LinkedHashMap<String, Set<String>>();
    private Map<String, Set<String>> filesClasses = new HashMap<String, Set<String>>();

    // maps class names to base class names
    private Map<String, String> classesBase = new HashMap<String, String>();

    // maps class names to the names of their derived classes (direct subclasses)
    private Map<String, Set<String>> classesDerived = new HashMap<String, Set<String>>();

    private Map<String, Set<String>> classesMembers = new LinkedHashMap<String, Set<String>>();

    // keys are formed as a combination of class name and member name
    private Map<String, ClassMemberType> classesMembersTypes = new HashMap<String, ClassMemberType>();

    // keys are formed as a combination of class name and member name
    private Map<String, Set<String>> classesMembersFiles = new HashMap<String, Set<String>>();

    private Map<String, Set<String>> closureObjectsMembers = new LinkedHashMap<String, Set<String>>();

    // keys are formed as a combination of closure object name and member name
    private Map<String, ClassMemberType> closureObjectsMembersTypes = new HashMap<String, ClassMemberType>();


    public GlobalParsingStats() {
    }


    public void addParsingStats(ParsingStats stats, String fileName) {

        if ( fileName.equals("-") ) {
            fileName = "<stdin>";
        }

        files.add(fileName);

        Set<String> functions = stats.getFunctions();

        for ( String function : functions ) {
            functionsFiles.put(function, fileName);
        }

        filesFunctions.put(fileName, functions);

        Set<String> closureObjects = stats.getClosureObjects();

        for ( String closureObject : closureObjects ) {
            Set<String> members = stats.getClosureObjectMembers(closureObject);
            Set<String> closureObjectMembersFoundBefore = closureObjectsMembers.get(closureObject);

            if ( closureObjectMembersFoundBefore == null ) {
                closureObjectMembersFoundBefore = new LinkedHashSet<String>();
                closureObjectsMembers.put(closureObject, closureObjectMembersFoundBefore);
            }

            for ( String member : members ) {
                ClassMemberType type = stats.getClosureObjectMemberType(closureObject, member);

                closureObjectMembersFoundBefore.add(member);
                this.closureObjectsMembersTypes.put(getClassMemberKey(closureObject, member), type);
            }

            closureObjectFiles.put(closureObject, fileName);
        }

        filesClosureObjects.put(fileName, closureObjects);


        Set<String> classes = stats.getClasses();

        for ( String className : classes ) {
            String baseClass = stats.getClassBase(className);
            Set<String> derived = stats.getDerivedClasses(className);
            Set<String> derivedFoundBefore = classesDerived.get(className);
            Set<String> classFiles = classesFiles.get(className);
            Set<String> classMembers = stats.getClassMembers(className);
            Set<String> classMembersFoundBefore = classesMembers.get(className);

            if ( classFiles == null ) {
                classFiles = new LinkedHashSet<String>();
                classesFiles.put(className, classFiles);
            }

            classFiles.add(fileName);

            if ( classMembersFoundBefore == null ) {
                classMembersFoundBefore = new LinkedHashSet<String>();
                classesMembers.put(className, classMembersFoundBefore);
            }

            for ( String member : classMembers ) {
                String memberKey = getClassMemberKey(className, member);
                ClassMemberType type = stats.getClassMemberType(className, member);

                classMembersFoundBefore.add(member);
                classesMembersTypes.put(memberKey, type);

                Set<String> classMemberFilesAddedBefore = classesMembersFiles.get(memberKey);

                if ( classMemberFilesAddedBefore == null ) {
                    classMemberFilesAddedBefore = new LinkedHashSet<String>();
                    classesMembersFiles.put(memberKey, classMemberFilesAddedBefore);
                }

                classMemberFilesAddedBefore.add(fileName);

                // TODO: what if the member is defined in different files and with a different types??
            }

            if ( baseClass != null )
                classesBase.put(className, baseClass);

            if ( derivedFoundBefore == null ) {
                derivedFoundBefore = new LinkedHashSet<String>();
                classesDerived.put(className, derivedFoundBefore);
            }

            derivedFoundBefore.addAll(derived);
        }

        filesClasses.put(fileName, classes);
    }


    public Set<String> getFiles() {
        return files;
    }


    public boolean fileHasFunction(String fileName, String function) {
        return getFileFunctions(fileName).contains(function);
    }


    public boolean fileHasClosureObject(String fileName, String closureObject) {
        return getFileClosureObjects(fileName).contains(closureObject);
    }


    public boolean fileHasClass(String fileName, String className) {
        Set<String> classes = filesClasses.get(fileName);

        return ( classes == null ) ? false : classes.contains(className);
    }


    public Set<String> getFunctions() {
        List<String> result = new ArrayList<String>(functionsFiles.keySet());

        Collections.sort(result, new ClassNameComparator());

        return new LinkedHashSet<String>(result);
    }


    public Set<String> getClosureObjects() {
        List<String> result = new ArrayList<String>(closureObjectFiles.keySet());

        Collections.sort(result, new ClassNameComparator());

        return new LinkedHashSet<String>(result);
    }


    public Set<String> getFileFunctions(String fileName) {
        Set<String> functions = filesFunctions.get(fileName);

        return ( functions != null ) ? functions : new LinkedHashSet<String>();
    }


    public Set<String> getFileClosureObjects(String fileName) {
        Set<String> closureObjects = filesClosureObjects.get(fileName);

        return ( closureObjects != null ) ? closureObjects : new LinkedHashSet<String>();
    }


    public Set<String> getClosureObjectMembers(String closureObject) {
        Set<String> members = closureObjectsMembers.get(closureObject);

        return ( members != null ) ? members : new LinkedHashSet<String>();
    }


    public Set<String> getClosureObjectMembers(String closureObject, ClassMemberType type) {
        Set<String> allMembers = getClosureObjectMembers(closureObject);
        Set<String> members = new LinkedHashSet<String>();

        for ( String member : allMembers ) {
            if ( isClosureObjectMemberOfType(closureObject, member, type) ) {
                members.add(member);
            }
        }

        return members;
    }


    public ClassMemberType getClosureObjectMemberType(String closureObject, String member) {
        return closureObjectsMembersTypes.get(getClassMemberKey(closureObject, member));
    }


    public boolean isClosureObjectMemberOfType(String closureObject, String member, ClassMemberType ofType) {
        ClassMemberType type = getClosureObjectMemberType(closureObject, member);

        return ( type == null ) ? false : ( type == ofType );
    }


    public String getClosureObjectFile(String closureObject) {
        return closureObjectFiles.get(closureObject);
    }


    public String getFunctionFile(String function) {
        return functionsFiles.get(function);
    }


    public Set<String> getClasses() {
        return classesFiles.keySet();
    }


    public Set<String> getClassesOrderedByHierarchyLevelAsc() {
        return getClassesOrderedByHierarchyLevel(true);
    }


    public Set<String> getClassesOrderedByHierarchyLevelDesc() {
        return getClassesOrderedByHierarchyLevel(false);
    }


    public boolean hasClass(String className) {
        return classesFiles.containsKey(className);
    }


    public Set<String> getFileClasses(String fileName) {
        Set<String> classes = filesClasses.get(fileName);

        return ( classes != null ) ? classes : new LinkedHashSet<String>();
    }


    /**
     * Returns a set containing all the members defined by a class (excludes inherited members).
     * 
     * @param className Class name.
     * @return A set containing all the members defined by the class (excludes inherited members).
     */
    public Set<String> getClassMembers(String className) {
        Set<String> members = classesMembers.get(className);

        return ( members != null ) ? members : new LinkedHashSet<String>();
    }


    /**
     * Returns a set containing all the members, of a given type, defined by a class (excludes inherited members).
     * 
     * @param className Class name.
     * @param ofType Member type.
     * @return A set containing all the members, of a given type, defined by the class (excludes inherited members).
     */
    public Set<String> getClassMembers(String className, ClassMemberType ofType) {
        Set<String> allMembers = getClassMembers(className);
        Set<String> members = new LinkedHashSet<String>();

        for ( String member : allMembers ) {
            if ( isClassMemberOfType(className, member, ofType) ) {
                members.add(member);
            }
        }

        return members;
    }


    public Set<String> getFilesWhereClassMemberIsDefined(String className, String member) {
        return classesMembersFiles.get(getClassMemberKey(className, member));
    }


    public boolean isClassMemberOfType(String className, String member, ClassMemberType type) {
        ClassMemberType memberType = getClassMemberType(className, member);

        return ( memberType == null ) ? false : ( memberType == type );
    }


    public ClassMemberType getClassMemberType(String className, String member) {
        return classesMembersTypes.get(getClassMemberKey(className, member));
    }


    public boolean classHasMember(String className, String member) {
        Set<String> members = getClassMembers(className);

        return members.contains(member);
    }


    public boolean isClassMemberOverriding(String className, String member) {
        boolean classHasMember = classHasMember(className, member);
        String upperClass = findFirstUpperClassWithMember(className, member);

        return classHasMember && (upperClass != null);
    }


    public boolean classHasInheritedMember(String className, String member) {
        boolean classHasMember = this.classHasMember(className, member);
        String upperClass = findFirstUpperClassWithMember(className, member);

        return !classHasMember && (upperClass != null);
    }


    public String getClassBase(String className) {
        return classesBase.get(className);
    }


    public Set<String> getClassInheritedMembers(String className) {
        Set<String> inherited = new LinkedHashSet<String>();
        Set<String> ownMembers = getClassMembers(className);
        List<String> upperClasses = getClassBaseHierarchy(className);

        for ( String upperClass : upperClasses ) {
            Set<String> upperMembers = getClassMembers(upperClass);

            for ( String upperMember : upperMembers ) {
                if ( !ownMembers.contains(upperMember) ) {
                    inherited.add(upperMember);
                }
            }
        }

        return inherited;
    }


    public Set<String> getClassInheritedMembers(String className, ClassMemberType memberType) {
        Set<String> result = new LinkedHashSet<String>();
        Set<String> allInherited = getClassInheritedMembers(className);

        for ( String member : allInherited ) {
            String upperClass = findFirstUpperClassWithMember(className, member);

            if ( upperClass == null ) {
                // should never happen in this context
                continue;
            }

            if ( !isClassMemberOfType(upperClass, member, memberType) ) {
                continue;
            }

            result.add(member);
        }

        return result;
    }


    public Set<String> getClassOverridingMembers(String className) {
        Set<String> overrided = new LinkedHashSet<String>();
        Set<String> ownMembers = getClassMembers(className);
        List<String> upperClasses = getClassBaseHierarchy(className);

        for ( String member : ownMembers ) {
            for ( String upperClass : upperClasses ) {
                if ( classHasMember(upperClass, member) ) {
                    overrided.add(member);
                    break;
                }
            }
        }

        return overrided;
    }


    public Set<String> getClassOverridingMembers(String className, ClassMemberType memberType) {
        Set<String> result = new LinkedHashSet<String>();
        Set<String> allOverrided = getClassOverridingMembers(className);

        for ( String member : allOverrided ) {
            String upperClass = findFirstUpperClassWithMember(className, member);

            if ( upperClass == null ) {
                // should never happen in this context
                continue;
            }

            if ( !isClassMemberOfType(upperClass, member, memberType) ) {
                continue;
            }

            result.add(member);
        }


        return result;
    }


    /**
     * Returns a set containing all members of a class unique to that class (members that are not inherited and neither
     * override any upper class member).
     * 
     * @param className Class name.
     * @return A set containing all members of a class unique to that class.
     */
    public Set<String> getClassUniqueMembers(String className) {
        Set<String> result = new LinkedHashSet<String>();
        Set<String> classMembers = getClassMembers(className);
        Set<String> overridedMembers = getClassOverridingMembers(className);

        for ( String member : classMembers ) {

            if ( overridedMembers.contains(member) ) {
                continue;
            }

            result.add(member);
        }

        return result;
    }


    /**
     * Returns a set containing all members of a class that unique to that class (members that are not inherited and neither
     * override any upper class member) and of the specified type.
     * 
     * @param className Class name.
     * @param memberType Member type.
     * @return A set containing all members of a class unique to that class and of a specified type.
     */
    public Set<String> getClassUniqueMembers(String className, ClassMemberType memberType) {
        Set<String> result = new LinkedHashSet<String>();
        Set<String> allUnique = getClassUniqueMembers(className);

        for ( String member : allUnique ) {
            if ( !isClassMemberOfType(className, member, memberType) ) {
                continue;
            }

            result.add(member);
        }

        return result;
    }


    /**
     * Finds the upper class hierarchy of a target class.
     * 
     * @param className Class name.
     * @return A class names list ordered by the distance, in the class hierarchy,
     *         a parent class (direct or not) is relative to the given class name.
     */
    public List<String> getClassBaseHierarchy(String className) {
        List<String> list = new ArrayList<String>();
        String baseClass = classesBase.get(className);

        while ( baseClass != null ) {
            list.add(baseClass);
            baseClass = classesBase.get(baseClass);
        }

        return list;
    }


    public String findFirstUpperClassWithMember(String className, String member) {
        List<String> upperClasses = getClassBaseHierarchy(className);

        for ( String upperClass : upperClasses ) {

            if ( classHasMember(upperClass, member) ) {
                return upperClass;
            }
        }

        return null;
    }


    public Set<String> getDerivedClasses(String className) {
        Set<String> derived = classesDerived.get(className);

        return ( derived != null ) ? derived : new LinkedHashSet<String>();
    }


    private String getClassMemberKey(String className, String memberName) {
        return className + "_" + memberName;
    }


    private Set<String> getClassesOrderedByHierarchyLevel(boolean ascending) {
        List<String> result = new ArrayList<String>();
        List<String> currentLevel = new ArrayList<String>();
        ClassNameComparator comp = new ClassNameComparator();
        List<String> nextLevel = null;

        // find root classes (classes without base class)
        for ( String className : classesFiles.keySet() ) {
            if ( getClassBase(className) == null ) {
                currentLevel.add(className);
            }
        }

        while ( !currentLevel.isEmpty() ) {
            Collections.sort(currentLevel, comp);

            if ( ascending ) {
                result.addAll(currentLevel);
            }
            else {
                result.addAll(0, currentLevel);
            }

            nextLevel = new ArrayList<String>();

            for ( String className : currentLevel ) {
                Set<String> derived = getDerivedClasses(className);

                for ( String derivedClass : derived ) {
                    nextLevel.add(derivedClass);
                }
            }

            currentLevel = nextLevel;
        }

        return new LinkedHashSet<String>(result);
    }
}
